# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import logging


def extract_key(data, key_names):
  return tuple(data[k] for k in key_names)


class BitmexTable(object):
  def __init__(self, logger=None):
    self._logger = logger or logging.getLogger(__name__)
    self._tables = {}
    self._keys = {}
    self._handler = {
        'partial': self.on_partial,
        'insert': self.on_insert,
        'update': self.on_update,
        'delete': self.on_delete
    }

    self.callbacks = []  # action, table_name, key, data
    self.error_callback = lambda: None

  @property
  def table(self):
    return self._tables

  def delete_old_entry(self, num_keep_per_table=8192):
    for table_name, table in self._tables.items():
      if len(table) <= num_keep_per_table:
        self._logger.debug('table [%s]: %d entries' % (table_name, len(table)))
        continue

      self._logger.debug('table [%s]: %d entries. %d entries will be deleted' %
                         (table_name, len(table), len(table) - num_keep_per_table))
      entry_ts = sorted([(entry['timestamp'], key) for key, entry in table.items()], reverse=True)
      for _, key in entry_ts[num_keep_per_table:]:
        del table[key]

  def _invoke_callback(self, action, table_name, key, data):
    for callback in self.callbacks:
      try:
        callback(action, table_name, key, data)
      except Exception:
        self._logger.exception('Exception raised')

  def _requires_table(handler):
    def wrapped_handler(self, msg):
      table_name = msg['table']
      if table_name not in self._tables:
        self._logger.error('Unknown table: %s\n%s' % (table_name, str(msg)))
        self.error_callback()
        return
      key_names = self._keys[table_name]
      handler(self, table_name, key_names, msg)

    return wrapped_handler

  def on_partial(self, msg):
    table_name = msg['table']
    if table_name not in self._tables:
      self._tables[table_name] = {}

    key_names = msg['keys']
    if not key_names:
      self._logger.error('no keys\n%s' % str(msg))
      return
    self._keys[table_name] = key_names

    for elem in msg['data']:
      key = extract_key(elem, key_names)
      self._tables[table_name][key] = elem
      self._invoke_callback('partial', table_name, key, elem)

  @_requires_table
  def on_insert(self, table_name, key_names, msg):
    table = self._tables[table_name]
    for elem in msg['data']:
      key = extract_key(elem, key_names)
      if key in table:
        self._logger.error('Key %s already exists in table [%s]. Overwriting...' %
                           (str(key), table_name))
      table[key] = elem
      self._invoke_callback('insert', table_name, key, elem)

  @_requires_table
  def on_update(self, table_name, key_names, msg):
    table = self._tables[table_name]
    for elem in msg['data']:
      key = extract_key(elem, key_names)
      if key not in table:
        self._logger.error('Key %s does not exist in table [%s]' % (str(key), table_name))
        continue
      table_elem = table[key]
      for field, value in elem.items():
        table_elem[field] = value
      self._invoke_callback('update', table_name, key, table_elem)

  @_requires_table
  def on_delete(self, table_name, key_names, msg):
    table = self._tables[table_name]
    for elem in msg['data']:
      key = extract_key(elem, key_names)
      if key in table:
        table_elem = table[key]
        del table[key]
        self._invoke_callback('delete', table_name, key, table_elem)

  def update(self, msg):
    if 'action' not in msg or 'table' not in msg:
      return
    handler = self._handler[msg['action']]
    handler(msg)
